package com.googlecode.continuity.core.server.hibernate;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.gwtwidgets.server.spring.GWTRPCServiceExporter;
import org.gwtwidgets.server.spring.ReflectionUtils;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import net.sf.hibernate4gwt.core.HibernateBeanManager;
import net.sf.hibernate4gwt.gwt.HibernateRPCHelper;
import net.sf.hibernate4gwt.rebind.ProxyManager;

/**
 * GWTHibernateRPCServiceExporter handles incoming RPC requests for GWT services and takes care of both GWT and
 * Hibernate serialization. 
 *
 * @author Peter Schmitt <aragos@aragos.de>
 */
public class GWTHibernateRPCServiceExporter extends GWTRPCServiceExporter {
  private HibernateBeanManager beanManager;

  /**
   * Creates a new GWTHibernateRPCServiceExporter instance with the given bean manager.
   *
   * @param beanManager bean manager
   */
  public GWTHibernateRPCServiceExporter(HibernateBeanManager beanManager) {
    super();
    this.beanManager = beanManager;
  }

  /**
   * Overridden from {@link org.gwtwidgets.server.spring.GWTRPCServiceExporter} and invoked by the servlet code.  Adds
   * Hibernate decoupling to serialization process.
   */
  public String processCall(String payload) throws SerializationException {
    try {
      HibernateRPCHelper.initClassLoader();
      ProxyManager.getInstance().setProxyGenerator(new UbiquitousJavassistProxyGenerator());

      // Copy & pasted & edited from the GWT 1.4.3 RPC documentation
      RPCRequest rpcRequest = decodeRequest(payload);
      Method targetMethod = ReflectionUtils.getRPCMethod(service, serviceInterfaces, rpcRequest.getMethod());
      Object[] targetParameters = rpcRequest.getParameters();

      try {
        Object result = targetMethod.invoke(service, targetParameters);
        return encodeForSuccess(rpcRequest, result);
      } catch (IllegalArgumentException e) {
        SecurityException securityException = new SecurityException("Blocked attempt to invoke method " + targetMethod);
        securityException.initCause(e);
        throw securityException;
      } catch (IllegalAccessException e) {
        SecurityException securityException =
            new SecurityException("Blocked attempt to access inaccessible method " + targetMethod
                + (service != null ? " on service " + service : ""));
        securityException.initCause(e);
        throw securityException;
      } catch (InvocationTargetException e) {
        Throwable cause = e.getCause();
        if (!(cause instanceof IsSerializable)) {
          logger.warn(e);
        }
        return encodeForFailure(rpcRequest, cause);
      }
    } catch (IncompatibleRemoteServiceException e) {
      logger.warn(e.getMessage());
      return encodeForFailure(null, e);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Decodes a request coming from the server, merging all included Hibernate controlled objects with server side
   * instances.
   *
   * @param payload encoded RPC request
   * @return decoded request
   */
  protected RPCRequest decodeRequest(String payload) {
    RPCRequest rpcRequest = RPC.decodeRequest(payload, null, this);
    HibernateRPCHelper.parseInputParameters(rpcRequest, beanManager, getThreadLocalRequest().getSession());
    return rpcRequest;
  }

  /**
   * Encodes return value of an RPC request in case of success, decoupling Hibernate controlled objects from server
   * instances.
   *
   * @param request current RPC request
   * @param result rpc call return value
   * @return encoded return value
   * @throws SerializationException throws an exception if return value cannot be encoded
   */
  protected String encodeForSuccess(RPCRequest request, Object result) throws SerializationException {
    result = HibernateRPCHelper.parseReturnValue(result, beanManager);
    return RPC.encodeResponseForSuccess(request.getMethod(), result, request.getSerializationPolicy());
  }

  /**
   * Encodes the error state for a failed RPC call, decoupling included Hibernate controlled objects from server
   * instances.
   *
   * @param request current RPC request
   * @param cause failure cause
   * @return encoded failure return
   * @throws SerializationException throws an exception if failure return cannot be encoded
   */
  protected String encodeForFailure(RPCRequest request, Throwable cause) throws SerializationException {
    return RPC.encodeResponseForFailure(request != null ? request.getMethod() : null, cause);
  }
}
